﻿using System;
using System.Diagnostics;
using System.IO;
using System.Security;
using Microsoft.Win32;


namespace SeleniumOrderPathRunner {


    public sealed class SeleniumServer : IDisposable {
        

        private static volatile SeleniumServer instance;

        /// <summary>
        /// An object to perform double-check locking upon instance creation to 
        /// avoid dead-locks
        /// </summary>
        private static object syncRoot = new object();
        private Process seleniumServer = null;
        private bool isDisposed = false;
        private bool isProcessExited = false;

        private static String seleniumDirectory;
        private static String serverURL;
        private static String jarFilePath;
        private static TestRunner.TestCaseGroups[] testCases;
        private static String[] orderpathNames;

        public enum Browser {None=0, Firefox, IExplore, Safari, Chrome, GoogleChrome };
        public enum OrderAppEnvironment {None=0, Int, Test, Prod, Local, Load, Beta };

        private static Browser sessionBrowser;
        private static OrderAppEnvironment sessionEnvironment;



        private TestRunner testRunner;

        /// <summary>
        /// Prevents a default instance of the SeleniumServer class from being created.  Starts the Selenium Java server in a background hidden thread which can be killed
        /// most easily by stopping the execution in the shell window
        /// </summary>
        private SeleniumServer() {
            if (seleniumDirectory == null || serverURL == null || sessionBrowser == Browser.None || sessionEnvironment == OrderAppEnvironment.None || jarFilePath == null) {
                throw new ArgumentNullException();
            }
            string javaFileLocation = this.GetJavaFileLocation();

            // Start the selenium server
            this.seleniumServer = new Process();
            this.seleniumServer.StartInfo.FileName = "\""+javaFileLocation+"\"";
            this.seleniumServer.StartInfo.Arguments = " -jar " + "\"" + jarFilePath + "\"" + createJarArguments();
            this.seleniumServer.StartInfo.UseShellExecute = false;
            //this.seleniumServer.StartInfo.WorkingDirectory = "\"" + jarFilePath.Substring(0, jarFilePath.LastIndexOf("\\")) + "\"";
            //this.seleniumServer.StartInfo.UseShellExecute = true;//when set to true, workingDirectory starts in the .exe directory.
            this.seleniumServer.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
            this.seleniumServer.EnableRaisingEvents = true;
            this.seleniumServer.Exited += (s, e) => { 
                isProcessExited = true; Console.WriteLine("An exit event has been raised for the selenium server.jar process. The isProcessExited value: "+ isProcessExited); 
            };
            this.seleniumServer.Start();   
            this.isDisposed = false;
        }


        /// <summary>
        /// Finalizes an instance of the SeleniumServer class.
        /// </summary>
        ~SeleniumServer() {
            this.Dispose(false);
        }


  
        /// <summary>
        /// Gets a thread safe instance of the Selenium Server class.
        /// </summary>
        public static SeleniumServer Instance {
            get {
                // This approach ensures that only one instance is created and 
                // only when the instance is needed. Also, the variable is 
                // declared to be volatile to ensure that assignment to the 
                // instance variable completes before the instance variable can 
                // be accessed. Lastly, this approach uses a syncRoot instance 
                // to lock on, rather than locking on the type itself, to avoid 
                // deadlocks.
                // This double-check locking approach solves the thread 
                // concurrency problems while avoiding an exclusive lock in 
                // every call to the Instance property method. It also allows 
                // you to delay instantiation until the object is first 
                // accessed. In practice, an application rarely requires this 
                // type of implementation. In most cases, the static 
                // initialization approach is sufficient.
                if (instance == null) {
                    lock (syncRoot) {
                        if (instance == null) {
                            instance = new SeleniumServer();
                        }
                    }
                }
                return instance;
            }
        }

        /// <summary>
        /// Initializes field variables
        /// </summary>
        /// <param name="serverIP">The location of the selenium HTTP proxy that will run the tests. Hard coded to be localhost for now</param>
        /// <param name="browserType">The web browser to run the tests on</param>
        /// <param name="testEnvironment">The orderpath environment to run the tests on</param>
        /// <param name="testCases">The testCase groups for the client to run. These groups are defined in the TestRunner Enum TestCaseGroups. If empty, all tests are run</param>
        /// <param name="orderpathNames">A list of orderpath names to be used in testing. Any Orderpaths in the default list that don't match a name in this list are not run. If empty, all Orderpaths are run</param>
        public static void configureNewInstance( String serverIP, String browserType, String testEnvironment, String[] testCaseList, String[] orderpathNameList) {
            seleniumDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Replace(@"file:\","") + Path.DirectorySeparatorChar;//Points to the directory that holds the executable
            serverURL = serverIP;
            jarFilePath = seleniumDirectory  + "selenium-server.jar";
            testCases = new TestRunner.TestCaseGroups[testCaseList.Length];
            for (int i = 0; i < testCaseList.Length; i++) {
                testCases[i] = (TestRunner.TestCaseGroups)Enum.Parse(typeof(TestRunner.TestCaseGroups), testCaseList[i], true);
            }
            orderpathNames = orderpathNameList;
            sessionEnvironment = (OrderAppEnvironment)Enum.Parse(typeof(OrderAppEnvironment), testEnvironment, true);
            sessionBrowser = (Browser)Enum.Parse(typeof(Browser), browserType.Substring(1), true);
        }


        /// <summary>
        /// Generates commandline arguments for the selenium-server.jar execution.
        /// The arguments are dependent upon the browser type, because they are
        /// designed to circumvent SSL problems by using ignore commands, 
        /// profiles with exceptions already added and so on, based on the browser
        /// used.
        /// </summary>
        /// <returns></returns>
        private String createJarArguments() {
            String jarArguments = " -userExtensions \""+ seleniumDirectory+"user-extensions.js\"";
            
            switch (sessionBrowser) {
                case Browser.Firefox:
                    String firefoxProfilesPath;

                if(Environment.OSVersion.ToString().Contains("Unix")) {
                    firefoxProfilesPath = getMacFirefoxProfilesLocation();
                } else {
                    firefoxProfilesPath = getWinFirefoxProfilesLocation();
                }

                switch (sessionEnvironment) {
                    case OrderAppEnvironment.Int:
                    jarArguments += " -firefoxProfileTemplate " + "\"" + Path.Combine( firefoxProfilesPath, @"seleniumFirefoxProfileInt" ) + "\"";
                    break;
                    case OrderAppEnvironment.Local:
                    jarArguments += " -firefoxProfileTemplate " + "\"" + Path.Combine( firefoxProfilesPath, @"seleniumFirefoxProfileInt" ) + "\"";
                    break;
                    case OrderAppEnvironment.Prod:
                    jarArguments += " ";//prod payflow? If so, might need dif profile. Investigate later
                    break;
                    case OrderAppEnvironment.Test:
                    jarArguments += " -firefoxProfileTemplate " + "\"" + Path.Combine( firefoxProfilesPath, @"seleniumFirefoxProfileInt" ) + "\"";
                    break;
                    case OrderAppEnvironment.Load:
                    jarArguments += " -firefoxProfileTemplate " + "\"" + Path.Combine( firefoxProfilesPath, @"seleniumFirefoxProfileInt" ) + "\"";
                    break;
                    case OrderAppEnvironment.Beta:
                    jarArguments += " -firefoxProfileTemplate " + "\"" + Path.Combine( firefoxProfilesPath, @"seleniumFirefoxProfileInt" ) + "\"";
                    //prod payflow? If so, might need dif profile. Investigate later
                    break;
                    default:
                    jarArguments += " ";
                    break;
                }
                return jarArguments;

                case Browser.IExplore:
                    return "-trustAllSSLCertificates";
                case Browser.Chrome:
                    return "-trustAllSSLCertificates";
                case Browser.GoogleChrome:
                    String googleChromeProfilesPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data\");
                    String googleChromeProfileSwitchStatement = "";
                    //NO SUPPORT FOR SPECIFYING PROFILE
                    
                    return "-trustAllSSLCertificates";
                default:
                    return "";
            }
        }


        private String getMacFirefoxProfilesLocation() {
            return @"~" + Path.DirectorySeparatorChar + @"Library" + Path.DirectorySeparatorChar + @"Application Support" +
                Path.DirectorySeparatorChar + @"Firefox" + Path.DirectorySeparatorChar + @"Profiles";            
        }


        private String getWinFirefoxProfilesLocation() {
            return Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 
                @"Mozilla\Firefox\Profiles\");
        }



        /// <summary>
        /// Instantiates a new instance of the test execution class and runs its tests then cleans up the testRunner object
        /// </summary>
        public void runTests() {
            testRunner = new TestRunner(seleniumDirectory, serverURL, sessionBrowser, sessionEnvironment, testCases, orderpathNames);
            testRunner.runTests();
        }




        /// <summary>
        /// Dispose of the class and stop the Selenium server process.
        /// Explicitely calling Dispose rather than letting the garbage
        /// collector call the finalizer whenever will also address
        /// any unmanaged resource deallocation.
        /// </summary>
        public void Dispose() {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Stops the process.
        /// </summary>
        /// <param name="disposing">
        /// True if managed resources need to be disposed
        /// </param>
        private void Dispose(bool disposing) {
            if (!this.isDisposed) {//prevents multiple calls to deallocate the same resources(ex: implicit dispose via finalizer and explicit dispose call) 
                if (disposing) {
                    // Code to dispose of any managed resources of the class
                    //open connection to .txt?
                    //open connection to .xls?
                }

                // Kill the server
                if (this.seleniumServer != null && !isProcessExited) {
                    try {
                        this.seleniumServer.Kill();
                        this.seleniumServer = null;
                    } catch (Exception e) {
                        String message = "The Selenium Java Server could not be stopped, please start Task Manager and kill the java process";
                        throw new Exception(message, e);
                    }
                }else if (this.seleniumServer != null && isProcessExited){
                    //for debugging only, the process has little useful information once exited
                }
            }

            this.isDisposed = true;
        }

        /// <summary>
        /// Attempts to get the Java installation folder from the registry.  If 
        /// it cannot be found the default installation folder is checked before 
        /// failing.
        /// </summary>
        /// <returns>The Java executable file path.</returns>
        /// <exception cref="System.Exception">
        /// Thrown when the user does not have permission to access the 
        /// registry.
        /// </exception>
        /// <exception cref="System.ObjectDisposedException">
        /// Thrown if the registry key object is disposed of.
        /// </exception>
        /// <exception cref="System.IOException">
        /// Thrown if the registry key object is marked for deletion.
        /// </exception>
        private string GetJavaFileLocation() {
            if(Environment.OSVersion.ToString().Contains("Unix")) {
                return getMacJavaLocation();
            } else {
                return getWinJavaLocation();
            }
        }





        private String getMacJavaLocation() {
            char dirSep = Path.DirectorySeparatorChar;
            return dirSep + @"System" + dirSep + @"Library" + dirSep + @"Frameworks" + dirSep + @"JavaVM.framework" +
                dirSep + @"Versions" + dirSep + @"CurrentJDK" + dirSep + @"Home" + dirSep + @"bin" + dirSep + @"java";
            //check that it exists and look for others
        }


        /// <summary>
        /// Attempts to get the Java installation folder from the registry.  If 
        /// it cannot be found the default installation folder is checked before 
        /// failing.
        /// </summary>
        /// <returns>The Java executable file path.</returns>
        /// <exception cref="System.Exception">
        /// Thrown when the user does not have permission to access the 
        /// registry.
        /// </exception>
        /// <exception cref="System.ObjectDisposedException">
        /// Thrown if the registry key object is disposed of.
        /// </exception>
        /// <exception cref="System.IOException">
        /// Thrown if the registry key object is marked for deletion.
        /// </exception>
        private String getWinJavaLocation() {
            string javaFileLocation = string.Empty;

            RegistryKey regKey = Registry.LocalMachine;
            RegistryKey subKey = null;
            try {
                // Check for Java in the native bitness
                string javaRegistryLocation = @"SOFTWARE\JavaSoft\Java Runtime Environment\";
                subKey = regKey.OpenSubKey(javaRegistryLocation);

                // Check if we are running in WOW64 and only 32 bit Java is installed
                if(subKey == null) {
                    javaRegistryLocation = @"SOFTWARE\Wow6432Node\JavaSoft\Java Runtime Environment\";
                    subKey = regKey.OpenSubKey(javaRegistryLocation);
                }

                // If Java was not found in either of these location the user 
                // needs to install it.  Note that Java 32 bit is a prerequisite 
                // for the installer so should always be installed
                if(subKey == null) {
                    throw new Exception("No installation of Java was detected to run the selenium server.");
                }

                // Get all the sub keys (could be multiple versions of Java 
                // installed) and get the most recent (last one)
                string[] subKeyNames = subKey.GetSubKeyNames();
                subKey = regKey.OpenSubKey(javaRegistryLocation + subKeyNames[subKeyNames.Length - 1]);

                // Specify the java executable location
                javaFileLocation = subKey.GetValue("JavaHome").ToString() + @"\bin\java.exe";
            } catch(SecurityException e) {
                // Attempt to find the java executable at the default location
                javaFileLocation = @"C:\Program Files\Java\jre6\bin\java.exe";
                if(!File.Exists(javaFileLocation)) {
                    throw new Exception("The program did not have permission to access the registry to obtain the installation folder of Java.", e);
                }
            } catch(UnauthorizedAccessException e) {
                // Attempt to find the java executable at the default location
                javaFileLocation = @"C:\Program Files\Java\jre6\bin\java.exe";
                if(!File.Exists(javaFileLocation)) {
                    throw new Exception("The user does not have the necessary registry rights to obtain the installation folder of Java.", e);
                }
            } catch(ObjectDisposedException e) {
                // This hopefully shouldn't happen so ask the user to report it
                throw new ObjectDisposedException("The Java registry object was closed.  Please report this error.", e);
            } catch(IOException e) {
                // This hopefully shouldn't happen so ask the user to report it
                throw new ObjectDisposedException("The Java registry object was marked for deletion.  Please report this error.", e);
            }

            return javaFileLocation;
        }
    }
}
